﻿using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using InTaPLC;
using IPort;

namespace YTTest
{
    public class AeltaPLC
    {
        /// <summary>
        /// 封装的最上层的 PLC串口发送方法；返回 想要的数据 或者 状态；
        /// </summary>
        /// <param name="串口"></param>
        /// <param name="PLC地址"></param>
        /// <param name="功能码"></param>
        /// <param name="寄存器"></param>
        /// <param name="寄存器号"></param>
        /// <param name="数据"></param>
        /// <param name="数据字节数"></param>
        /// <returns></returns>
        static public byte[] 发送指令(ILogicPort 串口, 从站地址 PLC地址, 命令码 功能码, 起始地址 寄存器 = 起始地址.D, int 寄存器号 = 0, byte[] 数据 = null, int 数据字节数 = 0)
        {
            byte 发送状态 = 0x01;
            byte[] 返回结果 = null;

            for (int i = 0; i < 3; i++)
            {
                发送状态 = 发指令包(串口, PLC地址, 功能码, 寄存器, 寄存器号, 数据);
                IOHelper.DebugMsg.Log("PLC   发送指令：" + 发送状态.ToString());
                if (发送状态 == 0x00)       // 发送成功，读取数据
                {
                    //Thread.Sleep(500);       // 延时等待数据返回
                    返回结果 = 接收数据(串口);
                    if (返回结果 == null)
                    {
                        IOHelper.DebugMsg.Log("PLC   发送指令： 返回结果 null");
                    }
                    else
                    {
                        IOHelper.DebugMsg.Log("PLC   发送指令：" + BitConverter.ToString(返回结果));
                    }

                    return 返回结果;
                }
            }
            return null;    // 循环3次均发送失败
        }

        /// <summary>
        /// 封装的PLC串口发送指令：
        /// </summary>
        /// <param name="PLC地址"> PLC从站地址 </param>
        /// <param name="功能码"> PLC操作 对应的命令码 </param>
        /// <param name="寄存器"> PLC装置基础地址：对应寄存器类型 </param>
        /// <param name="寄存器号"> 装置偏移地址：寄存器编号 </param>
        /// <param name="数据">发送的数据：字节数、寄存器值，等； </param>
        /// <returns> 发送结果：成功，0；失败，1； </returns>
        static private byte 发指令包(ILogicPort 串口, 从站地址 PLC地址, 命令码 功能码, 起始地址 寄存器 = 起始地址.D, int 寄存器号 = 0, byte[] 数据 = null)
        {
            try
            {
                while (串口.BytesCanRead > 0)      // 发送之前，先清空接收缓冲区
                {
                    串口.ReadData();
                }

                byte[] 包 = 字节数组转ASCII(组指令包(PLC地址, 功能码, 寄存器, 寄存器号, 数据));
                string debugstr = BitConverter.ToString(包);
                串口.WriteData(包);       // 发送最终数据
                return 0x00;    // 发送成功
            }
            catch (Exception e)
            {
                return 0x01;    // 发送失败
            }
        }

        /// <summary>
        /// 读串口缓冲区，接收返回的数据；指定接收的数据字节数；虽然指定也没用，主要还是依据结束字符和超时，来判断。
        /// </summary>
        /// <param name="串口"> 指定对应的串口 </param>
        /// <param name="数据字节数"> 想要获得的数据字节数，可以不指定，收到多少返回多少 </param>
        /// <param name="等待时间"> 接收等待的时间，超时即判定为通信出错 </param>
        /// <returns></returns>
        static private byte[] 接收数据(ILogicPort 串口, int 数据字节数 = 0, int 等待时间 = 500)
        {
            List<byte> buffer = new List<byte>();
            int index = 0;
            while (true)    // 等待一定时间  看看串口是否有响应；但是如果已经接收到足够/指定的数据，则直接停止
            {
                Thread.Sleep(30);
                index++;
                if (index >= 3)
                    break;
                IOHelper.DebugMsg.Log("PLC   串口.BytesCanRead：" + 串口.BytesCanRead);
                if (串口.BytesCanRead > 0)         // 如果有接收到数据
                {
                    Byte[] bufferb = 串口.ReadData();
                    IOHelper.DebugMsg.Log("PLC   串口数据：" + BitConverter.ToString(bufferb));
                    if (bufferb == null) continue;
                    IOHelper.DebugMsg.Log("PLC   串口.bufferb：" + bufferb.Length);

                    buffer.AddRange(bufferb);
                    if (buffer.Count <= 3) continue;
                    for (int i = 0; i < buffer.Count; i++)
                    {
                        if (buffer[0] != 0x3a)
                            buffer.RemoveAt(0);
                    }
                    if (buffer[buffer.Count - 1] != 0x0a)
                        continue;
                    else
                        break;
                }
                //return 接收的数据.Skip(0).Take(i + 1).ToArray();     // 不知道为什么 Skip 用不了
                //return 解指令包(接收的数据, i + 1);      // 索引为 i；所以字节数为 i+1
            }
            byte[] dta = buffer.ToArray();
            IOHelper.DebugMsg.Log("PLC   接收数据：" + BitConverter.ToString(dta));
            //3A-30-B1-30-33-30-B2-30-30-33-C3-42-C5-8D-0A
            //dta = new byte[15];
            //dta[0]= 0x3A;
            //dta[1] = 0x30;
            //dta[2] = 0xB1;
            //dta[3] = 0x30;
            //dta[4] = 0x33;
            //dta[5] = 0x30;
            //dta[6] = 0xB2;
            //dta[7] = 0x30;
            //dta[8] = 0x30;
            //dta[9] = 0x33;
            //dta[10] = 0xC3;
            //dta[11] = 0x42;
            //dta[12] = 0xC5;
            //dta[13] = 0x8D;
            //dta[14] = 0x0A;
            byte[] ss = 解指令包(dta);
            return ss;
        }


        /// <summary>
        /// 将16进制的指令数组 转为最终需要发送的 ASCII码值数组
        /// </summary>
        /// <param name="指令包"> 不含起始字符、但含校验码的 指令数组 </param>
        /// <returns>准备发送的 最终数据 </returns>
        static private byte[] 字节数组转ASCII(byte[] 指令包)
        {
            if (指令包 == null)     // 如果指令为空，则返回空
                return null;

            int 字节数 = 指令包.Length * 2 + 1 + 1 + 1;       // 起始位 1、停止位 1+1
            byte[] 发送数据 = new byte[字节数];

            string 指令字符串 = ":";         // 将指令数组转为 字符串，以便转为 ASCII码数组
            for (int i = 0; i < 指令包.Length; i++)
                指令字符串 += 指令包[i].ToString("X2");     // "X2"：将 16进制 转为 字符串
            指令字符串 += "10";

            发送数据 = Encoding.Default.GetBytes(指令字符串);    // 将字符串 转为 16进制数组
            发送数据[字节数 - 2] = 0x0D;                       // 两字节 停止码
            发送数据[字节数 - 1] = 0x0A;

            return 发送数据;
        }

        /// <summary>
        /// 将指令有效部分的 ASCII码数组 转为 16进制 字节数组
        /// </summary>
        /// <param name="原ASCII组"> 指令包去除 起始位=字符和结束字符 </param>
        /// <returns></returns>
        static private byte[] ASCII转字节数组(byte[] 原ASCII组)
        {
            string ASCII码 = Encoding.ASCII.GetString(原ASCII组);
            if (ASCII码.Length % 2 != 0)
                return null;            // 转换的长度不是2的整数倍，表明数据出错；返回 空

            ASCII码 = ASCII码.Split('\r')[0];     // 需要转义？？   之前此处会报错，因为后续接了一串 其它字符；以'\r\n'分隔；
            byte[] 返回结果 = new byte[ASCII码.Length / 2];
            try
            {
                for (int i = 0; i < 返回结果.Length; i++)
                    返回结果[i] = Convert.ToByte(ASCII码.Substring(i * 2, 2), 16);   // 将string 转换为16进制的 byte
            }
            catch (Exception ex)
            {

            }
            return 返回结果;
        }

        /// <summary>
        /// 根据16进制的指令数组 计算得到LCR校验和
        /// </summary>
        /// <param name="指令"> 不含起始字符 的 指令数组 </param>
        /// <param name="裁剪"> 裁剪掉指令数组的后 多少个 字节 </param>
        static private byte 计算校验和(byte[] 指令, int 裁剪 = 0)
        {
            if (指令 == null)
                return 0x00;
            byte 校验和 = 0x00;
            int 字节数 = 指令.Length - 裁剪;       // 实际需要计算校验和的字节数 
            for (int i = 0; i < 字节数; i++)
                校验和 += 指令[i];
            校验和 = (byte)(~校验和 + 1);         // 按位取反 再加1
            return 校验和;
        }

        /// <summary>
        /// 核对校验和，判断指令是否正确；指令不含 起止字符
        /// </summary>
        /// <param name="指令"> 最后一字节为校验和 </param>
        /// <returns></returns>
        static private bool 核对校验和(byte[] 指令)
        {
            if (指令 == null)
                return false;
            byte 校验和 = 0x00;
            for (int i = 0; i < 指令.Length - 1; i++)
                校验和 += 指令[i];
            校验和 = (byte)(~校验和 + 1);
            if (校验和 == 指令[指令.Length - 1])
                return true;
            else
                return false;
        }

        /// <summary>
        /// 指定 地址、功能码、寄存器、数据 等，打包为一个指令包
        /// </summary>
        /// <param name="PLC地址"></param>
        /// <param name="功能码"></param>
        /// <param name="寄存器"></param>
        /// <param name="寄存器号"></param>
        /// <param name="数据"></param>
        /// <returns></returns>
        static private byte[] 组指令包(从站地址 PLC地址, 命令码 功能码, 起始地址 寄存器 = 起始地址.D, int 寄存器号 = 0, byte[] 数据 = null)
        {
            if (数据 == null)
                return null;
            byte[] 指令包 = new byte[数据.Length + 5];       // 指令包长度：数据 + 1 + 1 + 2 + 1；加上校验和；
            int 装置地址 = (int)寄存器 + 寄存器号;         // 计算出装置/寄存器 地址
            指令包[0] = (byte)PLC地址;
            指令包[1] = (byte)功能码;
            指令包[2] = (byte)(装置地址 >> 8);         // 右移8位，取16位数据的髙字节
            指令包[3] = (byte)(装置地址 % 256);        // 取16位数据的低字节

            for (int i = 4; i < 指令包.Length - 1; i++)     // 最后一字节是校验和，需要 -1
                指令包[i] = 数据[i - 4];
            // 计算 指令包 的前几个字节的校验和
            指令包[指令包.Length - 1] = 计算校验和(指令包, 1);
            return 指令包;
        }

        /// <summary>
        /// 解析原始指令包；主要用于 对接收的PLC返回数据进行解析，得到想要的数据：16进制；
        /// </summary>
        /// <param name="原始指令包"> 串口接收的 带起止字符 的数组，末尾可能有多余的字节 </param>
        /// <param name="字节数"> 有意义的字节数 </param>
        /// <returns></returns>
        static private byte[] 解指令包(byte[] 原始指令包, int 字节数 = 0)
        {
            IOHelper.DebugMsg.Log("PLC   原始指令包---"+ BitConverter.ToString(原始指令包));
            if (字节数 == 0)
                字节数 = 原始指令包.Length;
            //byte[] 返回结果 = null;
            if (字节数 <= 3)
            {
                IOHelper.DebugMsg.Log("PLC   字节数 <= 3");
                return null;// 字节数 不大于 3，说明出错了。
            }

            byte[] 裁剪指令包 = new byte[字节数 - 3];       // 裁剪掉 1个起始字符、2个结束字符；

            for (int i = 0; i < 字节数 - 3; i++)
                裁剪指令包[i] = 原始指令包[i + 1];
            byte[] 字节指令 = ASCII转字节数组(裁剪指令包);        // 实际 16进制的指令
            IOHelper.DebugMsg.Log("PLC   字节指令：" + BitConverter.ToString(字节指令));

            if (!核对校验和(字节指令))       // 校验和不正确，返回空
            {
                IOHelper.DebugMsg.Log("PLC   核对校验和：false");
                return null;
            }

            if (字节指令[1] == (byte)命令码.异常码返回)         // 返回出现异常
            {
                byte[] 返回结果 = { 字节指令[2] };
                IOHelper.DebugMsg.Log("PLC   返回结果：" + BitConverter.ToString(返回结果));
                return 返回结果;
            }
            if (字节指令[1] <= 0x03)         // 对读指令的返回
            {
                byte[] 返回结果 = new byte[字节指令[2]];
                for (int i = 0; i < 字节指令[2]; i++)
                    返回结果[i] = 字节指令[i + 3];          // 从第4个开始才是数据：i+3
                IOHelper.DebugMsg.Log("PLC   返回结果：" + BitConverter.ToString(返回结果));
                return 返回结果;
            }
            byte[] 返回 = { 字节指令[1] };         // 非读指令，随便返回一个字节：命令码
            IOHelper.DebugMsg.Log("PLC   返回：" + BitConverter.ToString(返回));
            return 返回;
            /*
            if (字节数 == 0)
                字节数 = 原始指令包.Length;
            //byte[] 返回结果 = null;
            if (字节数 <= 3)       // 字节数 不大于 3，说明出错了。
                return null;

            byte[] 裁剪指令包 = new byte[字节数 - 3];       // 裁剪掉 1个起始字符、2个结束字符；

            for (int i = 0; i < 字节数 - 3; i++)
                裁剪指令包[i] = 原始指令包[i + 1];
            byte[] 字节指令 = ASCII转字节数组(裁剪指令包);        // 实际 16进制的指令

            if (!核对校验和(字节指令))       // 校验和不正确，返回空
                return null;

            if (字节指令[1] == (byte)命令码.异常码返回)         // 返回出现异常
            {
                byte[] 返回结果 = { 字节指令[2] };
                return 返回结果;
            }
            if (字节指令[1] <= 0x03)         // 对读指令的返回
            {
                byte[] 返回结果 = new byte[字节指令[2]];
                for (int i = 0; i < 字节指令[2]; i++)
                    返回结果[i] = 字节指令[i + 3];          // 从第4个开始才是数据：i+3
                return 返回结果;
            }
            byte[] 返回 = { 字节指令[1] };         // 非读指令，随便返回一个字节：命令码
            return 返回;
            */
        }

        //// 默认为 使用串口1 读PLC1 的 D寄存器
        //// 读单个寄存器的值，16位 有符号的整型；结果为空时，返回 0：逻辑有点问题，该值可能会冲突（这是一个BUG）；
        //private int 读单寄存器的值(int 寄存器号)
        //{
        //    byte[] 数据 = { 0x00, 0x01 };
        //    int 值 = 0;
        //    byte[] 结果 = 串口通信.发送指令(串口1, 从站地址.PLC1, 命令码.读寄存器值, 起始地址.D, 寄存器号, 数据);
        //    if (结果 == null)
        //        值 = 0;
        //    else if (结果.Length == 2)
        //        值 = 获取有符号数(结果[0], 结果[1]); // 转换为 有符号的数
        //    return 值;
        //}

        #region 测试模块

        static public byte 发数据(byte[] 数据, SerialPort 串口)
        {
            try
            {
                int 单个数据 = 0;
                while (串口.BytesToRead > 0)      // 发送之前，先清空接收缓冲区
                    单个数据 = 串口.ReadByte();

                串口.Write(数据, 0, 数据.Length);       // 发送最终数据
            }
            catch // (Exception e)
            {
                return 0x01;    // 发送失败
            }
            return 0x00;    // 发送成功
        }

        static public byte[] 收数据(SerialPort 串口, int 数据字节数, int 等待时间 = 500)
        {
            //byte[] 接收的数据 = new byte[数据字节数];
            //int 单个数据 = 0;
            //int i = 0;      // 索引

            Single timeA = 1000 * DateTime.Now.Second + DateTime.Now.Millisecond;   // 当前时间，毫秒
            Single timeB = timeA;
            while (timeB <= timeA + 等待时间)    // 等待一定时间  看看串口是否有响应；但是如果已经接收到足够/指定的数据，则直接停止
            {
                timeB = 1000 * DateTime.Now.Second + DateTime.Now.Millisecond;
                if (timeB < timeA)         // 异常，应该不会出现这种情况
                    timeB += 60000;
                if (串口.BytesToRead > 0)         // 如果有接收到数据
                {
                    byte[] 接收的数据 = new byte[串口.BytesToRead];
                    串口.Read(接收的数据, 0, 接收的数据.Length);
                    return 接收的数据;
                    //单个数据 = 串口.ReadByte();
                    //接收的数据[i] = (byte)单个数据;

                    //// 接收到特定的结束字符之后就返回；i>1为了确保 i-1不出错；
                    //if (i > 1 && (i >= 数据字节数 -1 || (接收的数据[i - 1] == 0x0D && 接收的数据[i] == 0x0A)))    // 0x0D 0x0A
                    //    break;      // 跳出循环
                }
                //return 接收的数据.Skip(0).Take(i + 1).ToArray();     // 不知道为什么 Skip 用不了
                //return 接收的数据;      // 索引为 i；所以字节数为 i+1
            }
            // 串口无响应 或出错，返回 null；
            return null;
        }

        #endregion

    }
}
